//Copyright 2011 - Jonathan Allen

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Reflection;
using Granite.Metadata;

namespace Granite.Modeling.Internals
{
	/// <summary>
	/// This is the template for a model's backing store.
	/// </summary>
	public abstract class PropertyBagBase : INotifyPropertyChanged
	{
		readonly ClassMetadata m_Metadata;

		/// <summary>
		/// This fires for each changed property.
		/// </summary>
		public event PropertyChangedEventHandler PropertyChanged;

		/// <summary>
		/// This fires after one or more properties have been changed and their corresponding RevalidateProperty events have been handled. 
		/// </summary>
		public event EventHandler RevalidateObject;

		/// <summary>
		/// This fires after one or more properties have been changed. 
		/// </summary>
		public event PropertyChangedEventHandler RevalidateProperty;

		/// <summary>
		/// This is the template for a model's backing store.
		/// </summary>
		/// <param name="owner">Owning model, used to fetch metadata</param>
		protected PropertyBagBase(object owner)
		{
			if (owner == null)
				throw new ArgumentNullException("owner", "owner is null.");
			Contract.EndContractBlock();

			m_Metadata = MetadataCache.GetMetadata(owner.GetType());
		}

		/// <summary>
		/// Cached metadata for the associated model.
		/// </summary>
		public ClassMetadata Metadata
		{
			get
			{
				Contract.Ensures(Contract.Result<ClassMetadata>() != null);

				return m_Metadata;
			}
		}

		/// <summary>
		/// Fetches a value, creating it if it doesn't exist.
		/// </summary>
		/// <typeparam name="T">Expected type</typeparam>
		/// <param name="creationFunction">Function to execute if the property doesn't already exist.</param>
		/// <param name="propertyName">Case-sensitive property name</param>
		/// <returns></returns>
		/// <remarks>Executing the default function will trigger validation but not a property-changed event.</remarks>
		public T GetNew<T>(Func<T> creationFunction, [CallerMemberName] string propertyName)
		{
			if (creationFunction == null)
				throw new ArgumentNullException("creationFunction", "creationFunction is null.");
			if (propertyName == null)
				throw new ArgumentNullException("propertyName", "propertyName is null");
			if (String.IsNullOrEmpty(propertyName))
				throw new ArgumentException("propertyName is empty.", "propertyName");
			Contract.EndContractBlock();

			var result = GetValue(propertyName);
			if (result == Missing.Value)
			{
				result = creationFunction();
				Set(result, PropertySetModes.SetAsOriginal, propertyName);
			}
			try
			{
				return (T)result;
			}
			catch (NullReferenceException ex)
			{
				//This can occur if T is a non-nullable value type and the parent class is mal-formed.
				throw new InvalidOperationException("Property " + propertyName + " contains a null", ex);
			}
		}

		/// <summary>
		/// Fetches a value, creating it if it doesn't exist.
		/// </summary>
		/// <typeparam name="T">Expected type that has a parameterless constructor</typeparam>
		/// <param name="propertyName">Case-sensitive property name</param>
		/// <returns></returns>
		/// <remarks>Executing the constructor will trigger validation but not a property-changed event.</remarks>
		public T GetNew<T>([CallerMemberName] string propertyName)
						where T : new()
		{
			if (propertyName == null)
				throw new ArgumentNullException("propertyName", "propertyName is null");
			if (String.IsNullOrEmpty(propertyName))
				throw new ArgumentException("propertyName is empty", "propertyName");
			Contract.EndContractBlock();

			var result = GetValue(propertyName);
			if (result == Missing.Value)
			{
				result = Activator.CreateInstance<T>();
				Set(result, PropertySetModes.SetAsOriginal, propertyName);
			}
			try
			{
				return (T)result;
			}
			catch (NullReferenceException ex)
			{
				//This can occur if T is a non-nullable value type and the parent class is mal-formed.
				throw new InvalidOperationException("Property " + propertyName + " contains a null", ex);
			}
		}

		/// <summary>
		/// Fetches a value, returning Default(T) if it doesn't exist.
		/// </summary>
		/// <typeparam name="T">Expected type</typeparam>
		/// <param name="propertyName">Case-sensitive property name</param>
		/// <returns></returns>
		/// <remarks>Storing the default value will trigger validation but not a property-changed event.</remarks>
		public T Get<T>([CallerMemberName] string propertyName)
		{
			if (propertyName == null)
				throw new ArgumentNullException("propertyName", "propertyName is null");
			if (String.IsNullOrEmpty(propertyName))
				throw new ArgumentException("propertyName is empty", "propertyName");
			Contract.EndContractBlock();

			var result = GetValue(propertyName);
			if (result == Missing.Value)
			{
				result = default(T);
				Set(result, PropertySetModes.SetAsOriginal, propertyName);
			}
			try
			{
				return (T)result;
			}
			catch (NullReferenceException ex)
			{
				//This can occur if T is a non-nullable value type and the parent class is mal-formed.
				throw new InvalidOperationException("Property " + propertyName + " contains a null", ex);
			}
		}

		/// <summary>
		/// Fetches a value, using the default value it if it doesn't exist.
		/// </summary>
		/// <typeparam name="T">Expected type</typeparam>
		/// <param name="defaultValue">Default value to use</param>
		/// <param name="propertyName">Case-sensitive property name</param>
		/// <returns></returns>
		/// <remarks>Storing the default value will trigger validation but not a property-changed event.</remarks>
		public T Get<T>(T defaultValue, [CallerMemberName] string propertyName)
		{
			if (propertyName == null)
				throw new ArgumentNullException("propertyName", "propertyName is null");
			if (String.IsNullOrEmpty(propertyName))
				throw new ArgumentException("propertyName is empty", "propertyName");
			Contract.EndContractBlock();

			var result = GetValue(propertyName);
			if (result == Missing.Value)
			{
				result = defaultValue;
				Set(result, PropertySetModes.SetAsOriginal, propertyName);
			}
			try
			{
				return (T)result;
			}
			catch (NullReferenceException ex)
			{
				//This can occur if T is a non-nullable value type and the parent class is mal-formed.
				throw new InvalidOperationException("Property " + propertyName + " contains a null", ex);
			}
		}

		/// <summary>
		/// Implmentors need to override this to return the indicated value.
		/// </summary>
		/// <param name="propertyName">Name of the property to fetch.</param>
		/// <returns>The indicated value or System.Reflection.Missing if the value isn't defined.</returns>
		[Pure]
		public abstract object GetValue([CallerMemberName] string propertyName);

		/// <summary>
		/// This property indicates whether or not the assoicated property was created.
		/// </summary>
		/// <param name="propertyName"></param>
		/// <returns></returns>
		[Pure]
		public abstract bool IsDefined([CallerMemberName] string propertyName);

		/// <summary>
		/// Triggers the RevalidateObject event.
		/// </summary>
		protected virtual void OnRevalidateObject()
		{
			if (RevalidateObject != null)
				RevalidateObject(this, EventArgs.Empty);
		}

		/// <summary>
		/// Triggers the RevalidateProperty event.
		/// </summary>
		protected void OnRevalidateProperty(PropertyMetadata property)
		{
			if (property == null)
				throw new ArgumentNullException("property", "property is null.");

			if (RevalidateProperty != null)
				RevalidateProperty(this, property.PropertyChangedEventArgs);
		}

		/// <summary>
		/// Uses the property metadata to trigger validation on all model properties. Then requests validation on the object iteself.
		/// </summary>
		public void RevalidateAll()
		{
			foreach (var property in m_Metadata.Properties)
			{
				OnRevalidateProperty(property);
			}
			OnRevalidateObject();
		}

		/// <summary>
		/// Implementors need to override this to save the indicated value.
		/// </summary>
		/// <param name="value">The value to be saved. A null will set the value to null. System.Reflection.Missing will delete the field entirely.</param>
		/// <param name="mode">Indicates special handling for the action.</param>
		/// <param name="propertyName">Name of property to update</param>
		/// <returns>True if the value actually changed</returns>
		/// <remarks>This will create the property if it doesn't already exist</remarks>
		public abstract bool Set(object value, PropertySetModes mode, [CallerMemberName] string propertyName);

		/// <summary>
		/// Set the indicated property to the value.
		/// If the value doesn't match the previous value, or if there is no previous value, raise a property changed notification.
		/// </summary>
		/// <param name="propertyName">Name of property to be created/updated</param>
		/// <param name="value">Value to be saved.</param>
		/// <exception cref="System.ArgumentOutOfRangeException">An ArgumentOutOfRangeException is thrown if System.Reflection.Missing is passed as the value.</exception>
		public bool Set(object value, [CallerMemberName] string propertyName)
		{
			if (propertyName == null)
				throw new ArgumentNullException("propertyName", "propertyName is null");
			if (String.IsNullOrEmpty(propertyName))
				throw new ArgumentException("propertyName is empty.", "propertyName");
			Contract.EndContractBlock();

			return Set(value, PropertySetModes.RaiseChangedEvent | PropertySetModes.ValidateProperty | PropertySetModes.ValidateObject, propertyName);
		}

		///// <summary>
		///// If the property is listed in the owner's meta data this corrects the casing. Otherwise it returns the unaltered parameter.
		///// </summary>
		///// <param name="propertyName"></param>
		///// <returns></returns>
		//protected string FixCasing(string propertyName)
		//{
		//	if (m_Metadata.Properties.Contains(propertyName))
		//		return m_Metadata.Properties[propertyName].Name;
		//	else
		//		return propertyName;
		//}

		///// <summary>
		///// Raised when parsing a string value to a property. 
		///// </summary>
		//public event EventHandler<ParsingPropertyEventArgs> ParsingProperty;

		//internal object ParseType(string value, string propertyName)
		//{
		//	if (!m_Metadata.Properties.Contains(propertyName))
		//		return value;


		//	var eArgs = new ParsingPropertyEventArgs(value, m_Metadata.Properties[propertyName]);

		//	if (ParsingProperty != null)
		//		ParsingProperty(this, eArgs);

		//	if (eArgs.Handled)
		//		return eArgs.ParsedValue;

		//	var targetType = m_Metadata.Properties[propertyName].PropertyType;

		//	//Nullable<T> support
		//	if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>))
		//	{
		//		if (string.IsNullOrEmpty(value))
		//			return null;
		//		targetType = Nullable.GetUnderlyingType(targetType); //.GetGenericArguments()[0];
		//	}

		//	if (targetType == typeof(string))
		//		return value;
		//	if (targetType == typeof(bool))
		//		return bool.Parse(value);
		//	if (targetType == typeof(byte))
		//		return byte.Parse(value, eArgs.CultureInfo);
		//	if (targetType == typeof(sbyte))
		//		return sbyte.Parse(value, eArgs.CultureInfo);
		//	if (targetType == typeof(Int16))
		//		return Int16.Parse(value, eArgs.CultureInfo);
		//	if (targetType == typeof(UInt16))
		//		return UInt16.Parse(value, eArgs.CultureInfo);
		//	if (targetType == typeof(Int32))
		//		return Int32.Parse(value, eArgs.CultureInfo);
		//	if (targetType == typeof(UInt32))
		//		return UInt32.Parse(value, eArgs.CultureInfo);
		//	if (targetType == typeof(Int64))
		//		return Int64.Parse(value, eArgs.CultureInfo);
		//	if (targetType == typeof(UInt64))
		//		return UInt64.Parse(value, eArgs.CultureInfo);
		//	if (targetType == typeof(Single))
		//		return Single.Parse(value, eArgs.CultureInfo);
		//	if (targetType == typeof(double))
		//		return double.Parse(value, eArgs.CultureInfo);
		//	if (targetType == typeof(decimal))
		//		return decimal.Parse(value, eArgs.CultureInfo);
		//	if (targetType == typeof(DateTime))
		//		return DateTime.Parse(value, eArgs.CultureInfo);
		//	if (targetType == typeof(DateTimeOffset))
		//		return DateTimeOffset.Parse(value, eArgs.CultureInfo);

		//	if (targetType.IsEnum)
		//		return Enum.Parse(targetType, value, true);

		//	throw new FormatException("Cannot parse values of type " + targetType.FullName);
		//}

		/// <summary>
		/// Triggers the PropertyChanged event.
		/// </summary>
		protected internal void OnPropertyChanged(PropertyMetadata property)
		{
			if (property == null)
				throw new ArgumentNullException("property", "property is null.");
			Contract.EndContractBlock();

			if (PropertyChanged != null)
				PropertyChanged(this, property.PropertyChangedEventArgs);

			if (property.AffectsCalculatedFields)
				foreach (var affectedProperty in property.CalculatedFields)
				{
					Contract.Assume(affectedProperty != null);
					OnPropertyChanged(affectedProperty);
				}
		}

		/// <summary>
		/// Triggers the PropertyChanged event.
		/// </summary>
		protected internal void OnPropertyChanged(string propertyName)
		{
			if (String.IsNullOrEmpty(propertyName))
			{
				if (PropertyChanged != null)
					PropertyChanged(this, CommonProperties.Empty);
			}
			else
			{
				OnPropertyChanged(Metadata.Properties[propertyName]);
			}
		}

		[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode"), SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic"),]
		[ContractInvariantMethod]
		void ObjectInvariant()
		{
			Contract.Invariant(m_Metadata != null);
		}

	}
}

